﻿/* 
 * Copyright (c) 2009-2013, Andriy Syrov
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without modification, are permitted provided 
 * that the following conditions are met:
 * 
 * Redistributions of source code must retain the above copyright notice, this list of conditions and the 
 * following disclaimer.
 * 
 * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and 
 * the following disclaimer in the documentation and/or other materials provided with the distribution.
 *
 * Neither the name of Andriy Syrov nor the names of his contributors may be used to endorse or promote 
 * products derived from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED 
 * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A 
 * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY 
 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED 
 * TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 
 * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN 
 * IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 
 *   
 */

namespace Codeplex.InertialScroll
{
    using System;
    using System.Windows;
    using System.Windows.Controls;
    using System.Windows.Input;
    using System.Windows.Interactivity;
    using System.Windows.Threading;

    public class InertialScrollBehavior: Behavior<FrameworkElement>
    {
        public const int                                 REFRESH_INTERVAL = 10;

        private bool                                     m_drag;

        private Point                                    m_prev;
        private Point                                    m_p0;

        private DateTime                                 m_tprev;
        private DateTime                                 m_t0;
 
        private DispatcherTimer                          m_timer;

        private ScrollState                              m_x = new ScrollState(0, 0);
        private ScrollState                              m_y = new ScrollState(0, 0);
        private InertialScroll                           m_scroll = new InertialScroll();

        private Canvas                                   m_parent;

        protected override void OnAttached(
        )
        {
            base.OnAttached();

            AssociatedObject.SizeChanged += OnAssociatedObjectSizeChanged;

            AssociatedObject.MouseLeftButtonDown += OnMouseDown;
            AssociatedObject.MouseMove += OnMouseMove;
            AssociatedObject.MouseLeftButtonUp += OnMouseLeftButtonUp;

            m_timer = new DispatcherTimer();
            m_timer.Interval = TimeSpan.FromMilliseconds(REFRESH_INTERVAL);
            m_timer.Tick += OnTick;
        }

        protected override void OnDetaching(
        )
        {
            base.OnDetaching();
            m_timer.Stop();

            if (m_parent != null)
            {
                m_parent.SizeChanged -= OnParentSizeChanged;
            }

            AssociatedObject.SizeChanged -= OnAssociatedObjectSizeChanged;
            AssociatedObject.MouseLeftButtonDown -= OnMouseDown;
            AssociatedObject.MouseMove -= OnMouseMove;
            AssociatedObject.MouseLeftButtonUp -= OnMouseLeftButtonUp;

            m_timer.Tick -= OnTick;
        }

        private bool AttachParentCanvas(
        )
        {
            if (m_parent == null && AssociatedObject.Parent as Canvas != null)
            {
                m_parent = AssociatedObject.Parent as Canvas;
                m_parent.SizeChanged += OnParentSizeChanged;
            }

            return m_parent != null;
        }

        void OnAssociatedObjectSizeChanged(
            object                                      sender, 
            SizeChangedEventArgs                        e
        )
        {
            UpdateScrollState(0);
        }

        private void OnParentSizeChanged(
            object                                      sender, 
            SizeChangedEventArgs                        e
        )
        {
            UpdateScrollState(0);
        }

        void OnTick(
            object                                      sender, 
            EventArgs                                   e
        )
        {
            UpdateScrollState((DateTime.Now - m_t0).TotalMilliseconds);

            if (m_x.Mode == InertialMode.Finished && m_y.Mode == InertialMode.Finished)
            {
                m_timer.Stop();
            }
            m_t0 = DateTime.Now;
        }

        void UpdateScrollState(
            double                                     delta 
        )
        {
            
            if (AttachParentCanvas())
            { 
                m_x = m_scroll.Calculate(delta, m_x, AssociatedObject.ActualWidth, m_parent.ActualWidth);
                m_y = m_scroll.Calculate(delta, m_y, AssociatedObject.ActualHeight, m_parent.ActualHeight);

                var x = -m_x.Position;
                var y = -m_y.Position;
                AssociatedObject.SetXY(x, y);
            }
        }

        void OnMouseLeftButtonUp(object sender, MouseButtonEventArgs e)
        {
            Point                                     p;

            if (m_drag)
            {
                m_drag = false;
                m_timer.Start();

                p = e.GetPosition(m_parent);

                m_x.SetSpeed(p.X, m_prev.X, DateTime.Now, m_tprev);
                m_y.SetSpeed(p.Y, m_prev.Y, DateTime.Now, m_tprev);

                AssociatedObject.ReleaseMouseCapture();

                m_x = new ScrollState(m_x.Speed, -AssociatedObject.GetX());
                m_y = new ScrollState(m_y.Speed, -AssociatedObject.GetY());

                m_parent.Cursor = Cursors.Arrow;
                m_t0 = DateTime.Now;
            }
        }

        void OnMouseMove(
            object                                      sender, 
            MouseEventArgs                              e
        )
        {
            Point                                       p;

            if (m_drag)
            {
                p = e.GetPosition(m_parent);

                m_x.SetSpeed(p.X, m_p0.X, DateTime.Now, m_t0);
                m_y.SetSpeed(p.Y, m_p0.Y, DateTime.Now, m_t0);


                var x = AssociatedObject.GetX() + p.X - m_p0.X;
                var y = AssociatedObject.GetY() + p.Y - m_p0.Y;
                AssociatedObject.SetXY(x, y);

                m_prev  = m_p0;
                m_tprev = m_t0;
                m_t0    = DateTime.Now;
                m_p0    = p;
            }
        }

        void OnMouseDown(
            object                                      sender, 
            MouseButtonEventArgs                        e
        )
        {
            if (AttachParentCanvas())
            { 
                m_timer.Stop();

                m_drag = true;

                m_tprev = m_t0 = DateTime.Now;
                m_prev = m_p0 = e.GetPosition(m_parent);

                m_x.Speed = 0;
                m_y.Speed = 0;
                
                AssociatedObject.CaptureMouse();
                m_parent.Cursor = Cursors.Hand;
            }
        }
    }

    /// 
    /// <summary>
    /// Helper to get/set canvas left/top properties</summary>
    /// 
    public static class ElementEx
    {
        public static double GetX(
            this FrameworkElement                       el
        )
        {
            return (double) el.GetValue(Canvas.LeftProperty);
        }

        public static double GetY(
            this FrameworkElement                       el
        )
        {
            return (double) el.GetValue(Canvas.TopProperty);
        }

        public static void SetXY(
            this FrameworkElement                       el,
            double                                      x,
            double                                      y
        )
        {
            el.SetValue(Canvas.LeftProperty, x);
            el.SetValue(Canvas.TopProperty, y);
        }
    }
}
